const encryptor = {
    enc: new TextEncoder(),
    dec: new TextDecoder(),

    // aes-cbc-params
    getAesCbcParams(password) {
        return {
            "name": "AES-CBC",
            "iv": this.getIvByPassword(password)
        }
    },

    // iv and salt
    cachedIvs: {},
    cachedSalts: {},
    getIvByPassword(password) {
        if (!password) return Int8Array.from("1111222233334444");
        if (!this.cachedIvs[password]) {
            this.cachedIvs[password] = this.getUint8ArrayByPassword(password, 16);
        }
        return this.cachedIvs[password];
    },
    getSaltByPassword(password) {
        if (!password) return Int8Array.from("1111");
        if (!this.cachedSalts[password]) {
            this.cachedSalts[password] = this.getUint8ArrayByPassword(password, 4);
        }
        return this.cachedSalts[password];
    },
    getUint8ArrayByPassword(password, arrayLength) {
        var sb = this.enc.encode(password);
        var sbLength = sb.length;
        var buf = new Uint8Array(arrayLength);
        for (var i = 0, j = 0; i < arrayLength; i++) {
            buf[i] = sb[j];
            j = (j + 1) % sbLength;
        }
        return buf;
    },

    // keys
    cachedKeys: {},
    getCryptoKey(password, salt = "") {
        return new Promise((resolve, reject) => {
            window.crypto.subtle.importKey(
                "raw",
                this.enc.encode(password),
                "PBKDF2",
                false,
                ["deriveBits", "deriveKey"]
            ).then(keyMaterial => {
                window.crypto.subtle.deriveKey(
                    {
                        name: "PBKDF2",
                        salt: this.enc.encode(salt),
                        iterations: 100000,
                        hash: "SHA-256",
                    },
                    keyMaterial,
                    { name: "AES-CBC", length: 256 },
                    true,
                    ["encrypt", "decrypt"]
                ).then(cryptoKey => {
                    resolve(cryptoKey)
                }).catch(e => {
                    reject(e)
                })
            })
        })
    },
    getCachedCryptoKey(password, salt) {
        var k = password + salt;
        if (this.cachedKeys[k]) {
            return Promise.resolve(this.cachedKeys[k]);
        } else {
            return this.getCryptoKey(password, salt).then(key => {
                this.cachedKeys[k] = key;
                return key;
            })
        }
    },
    getTypedKey(cryptoKey) {
        return crypto.subtle.exportKey("raw", cryptoKey)
    },
    getRandomKey(length = 32) {
        return this.btoa(crypto.getRandomValues(new Uint8Array(length)))
    },

    // actions
    encrypt(originData, password) {
        if (!originData) return Promise.resolve("");
        if (!password) return Promise.reject("no password");

        return new Promise((resolve, reject) => {
            const salt = this.getSaltByPassword(password);
            this.getCachedCryptoKey(password, salt).then(cryptoKey => {
                const aesCbcParams = this.getAesCbcParams(password);
                crypto.subtle.encrypt(aesCbcParams, cryptoKey, this.enc.encode(originData))
                .then(arrayBufferData => {
                    resolve(this.btoa(arrayBufferData))
                }).catch(e => {
                    reject(e)
                })
            }).catch(e => {
                reject(e)
            })
        })
    },
    decrypt(secretBase64Data, password) {
        if (!secretBase64Data) return Promise.resolve("")
        if (!password || !secretBase64Data) return Promise.reject("no password");

        return new Promise((resolve, reject) => {
            const salt = this.getSaltByPassword(password);
            this.getCachedCryptoKey(password, salt).then(cryptoKey => {
                const aesCbcParams = this.getAesCbcParams(password);
                crypto.subtle.decrypt(aesCbcParams, cryptoKey, this.atob(secretBase64Data)).then(arrayBufferData => {
                    resolve(this.dec.decode(arrayBufferData))
                }).catch(e => {
                    reject(e)
                })
            }).catch(e => {
                reject(e)
            })
        })
    },

    // transform between base64 and bufferArray
    btoa(arrayBuffer) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
    },
    atob(base64Str) {
        const asciiStr = atob(base64Str)
        const typedArr = new Uint8Array(asciiStr.length);
        for (var i = 0, len = asciiStr.length; i < len; i++) {
            typedArr[i] = asciiStr.charCodeAt(i)
        }
        return typedArr
    },
    btoas(dataStr) {
        return this.btoa(this.env.encode(dataStr))
    },
    atobs(base64Str) {
        return this.dec.decode(this.atob(base64Str))
    },
}
